/*
 *  Copyright 2011 jmarsden.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package cc.plural.jsonij.marshal;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import cc.plural.jsonij.JSON;
import cc.plural.jsonij.Value;
import cc.plural.jsonij.marshal.JavaMarshaler.CycleDetector;
import cc.plural.jsonij.marshal.annotation.JSONEncoder;

/**
 *
 * @author jmarsden
 */
public class JavaObjectMarshaler {

    protected JavaMarshaler marshaler;
    final protected Map<Class<?>, Inspector> inspectedClasses;
    static JSONCodecStore codecStore;
    static int cycleLevels;

    static {
        cycleLevels = 0;
    }

    public JavaObjectMarshaler(JavaMarshaler marshaler) {
        this.marshaler = marshaler;
        inspectedClasses = new HashMap<Class<?>, Inspector>();
    }

    public boolean isObjectType(Class<?> objectClass) {
        Inspector inspector = JavaType.getInstpector(objectClass);
        InspectorProperty[] properties = inspector.getProperties();
        if (( properties != null && Array.getLength(properties) > 0 ) || inspector.hasInnerArray() || inspector.hasInnerObject()) {
            return true;
        }
        return false;
    }

    public Value marshalJavaObject(Object o, CycleDetector cycleDetector) {
        Class<?> objectClass = o.getClass();
        // Check for JSONEncoder Annotation
        Method[] methods = objectClass.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getAnnotation(JSONEncoder.class) != null) {
                // TODO: Encode using method.
            }
        }
        // Check for JSONCodec
        if (codecStore != null && codecStore.hasCodec(objectClass)) {
            @SuppressWarnings("rawtypes")
			JSONCodec codec = codecStore.getCodec(objectClass);
            @SuppressWarnings("unchecked")
			Value value = codec.encode(o);
            return value;
        }

        // Find an object inspector
        Inspector inspector = JavaType.getInstpector(objectClass);

        HashMap<String, Value> valueCollector = new HashMap<String, Value>();
        InspectorProperty[] properties = inspector.getProperties();
        String name = null;
        Value value = null;
        int propCount = 0;
        for (InspectorProperty property : properties) {
            if (!property.hasAccessor()) {
                continue;
            }
            name = property.getPropertyName();
            if (property.getAccessPropertyType() == InspectorProperty.TYPE.METHOD) {
                String accessorName = property.getAccessName();
                try {
                    Method method = objectClass.getMethod(accessorName);
                    value = marshalObjectMethodValue(method, o, cycleDetector);
                    if (value == null) {
                        continue;
                    }
                }
                catch (Exception ex) {
                    value = new JSON.String(ex.toString());
                }
            } else if (property.getAccessPropertyType() == InspectorProperty.TYPE.FIELD) {
                String accessorName = property.getAccessName();
                try {
                    Field field = objectClass.getField(accessorName);
                    value = marshalObjectFieldValue(field, o, cycleDetector);
                    if (value == null) {
                        continue;
                    }
                }
                catch (Exception ex) {
                    value = new JSON.String(ex.toString());
                }
            } else {
                value = JSON.NULL;
            }
            propCount++;
            valueCollector.put(name, value);
        }
        if (inspector.hasInnerArray()) {
            if (propCount > 0) {
                valueCollector.put("innerArray", marshaler.marshalJavaList(o, cycleDetector));
            } else {
                return marshaler.marshalJavaList(o, cycleDetector);
            }
        }
        if (inspector.hasInnerObject()) {
            if (propCount > 0) {
                valueCollector.put("innerObject", marshaler.marshalJavaMap(o, cycleDetector));
            } else {
                return marshaler.marshalJavaMap(o, cycleDetector);
            }
        }
        if (valueCollector.isEmpty()) {
            return null;
        } else {
            JSON.Object<JSON.String, Value> marshaledObject = new JSON.Object<JSON.String, Value>();
            Iterator<String> keySetIterator = valueCollector.keySet().iterator();
            while (keySetIterator.hasNext()) {
                String key = keySetIterator.next();
                marshaledObject.put(new JSON.String(key), valueCollector.get(key));
            }
            return marshaledObject;
        }
    }

    protected Value marshalObjectMethodValue(Method method, Object o, CycleDetector cycleDetector) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Value value = null;
        Object marshaledObject = method.invoke(o);
        if (marshaledObject == null) {
            value = null;
        } else {
            int hashCode = marshaledObject.hashCode();
            if (marshaledObject.getClass() == Boolean.class
                    || marshaledObject.getClass() == Byte.class
                    || marshaledObject.getClass() == Short.class
                    || marshaledObject.getClass() == Integer.class
                    || marshaledObject.getClass() == Float.class
                    || marshaledObject.getClass() == Double.class
                    || marshaledObject.getClass() == Long.class
                    || marshaledObject.getClass() == String.class) {
                value = marshaler.marshalAnyObject(marshaledObject, cycleDetector.cloneCycleDetector());
            } else if (!cycleDetector.hashDetected(hashCode) || cycleDetector.getHashCount(hashCode) < JavaObjectMarshaler.getCycleLevels()) {
                cycleDetector.addHash(hashCode);
                value = marshaler.marshalAnyObject(marshaledObject, cycleDetector.cloneCycleDetector());
            } else {
                value = null;
            }
        }
        return value;
    }

    protected Value marshalObjectFieldValue(Field field, Object o, CycleDetector cycleDetector) throws IllegalArgumentException, IllegalAccessException {
        Value value = null;
        Object marshaledObject = field.get(o);
        if (marshaledObject == null) {
            value = null;
        } else {
            int hashCode = marshaledObject.hashCode();
            if (marshaledObject.getClass() == Boolean.class
                    || marshaledObject.getClass() == Byte.class
                    || marshaledObject.getClass() == Short.class
                    || marshaledObject.getClass() == Integer.class
                    || marshaledObject.getClass() == Float.class
                    || marshaledObject.getClass() == Double.class
                    || marshaledObject.getClass() == Long.class
                    || marshaledObject.getClass() == String.class) {
                value = marshaler.marshalAnyObject(marshaledObject, cycleDetector.cloneCycleDetector());
            } else if (!cycleDetector.hashDetected(hashCode) || cycleDetector.getHashCount(hashCode) < JavaObjectMarshaler.getCycleLevels()) {
                cycleDetector.addHash(hashCode);
                value = marshaler.marshalAnyObject(marshaledObject, cycleDetector.cloneCycleDetector());
            } else {
                value = null;
            }
        }
        return value;
    }

    public static boolean hasCodec(Class<?> codecType) {
        if (codecStore == null) {
            return false;
        } else {
            return codecStore.hasCodec(codecType);
        }
    }

    public static void registerCodec(Class<?> codecClass) {
        if (codecStore == null) {
            codecStore = new JSONCodecStore();
        }
        codecStore.registerCodec(codecClass);
    }

    public static JSONCodec<?> getCodec(Class<?> codecType) {
        if (codecStore == null) {
            return null;
        } else {
            return codecStore.getCodec(codecType);
        }
    }

    public static int getCycleLevels() {
        return cycleLevels;
    }

    public static void setCycleLevels(int levels) {
        cycleLevels = levels;
    }
}
